SPB Git

spb/autoscholar

Public
1import { getRun } from "@autoscholar/core";2import { publicationDecisions, qualityGateResults, reviewFindings } from "@autoscholar/db";3import { asc, desc, eq } from "drizzle-orm";4import Link from "next/link";5import { notFound } from "next/navigation";6import { StatusBadge } from "@/components/StatusBadge";7import { fmtDateTime } from "@/lib/format";8import { db } from "@/lib/server/db";9import { OverrideForm } from "./OverrideForm";1011export const dynamic = "force-dynamic";12const NON_OVERRIDABLE = [13  "worker.real",14  "references.verified",15  "pdf.valid",16  "secrets.none",17  "attempt.final",18  "checksums.valid",19  "results.consistency",20  "data.provenance",21  "claims.traced",22  "contract.artifacts",23  "disclosure.present",24];2526export default async function QualityDetail({ params }: { params: Promise<{ runId: string }> }) {27  const { runId } = await params;28  const d = db();29  let data: Awaited<ReturnType<typeof getRun>>;30  try {31    data = await getRun(d, runId);32  } catch {33    notFound();34  }35  const gates = await d36    .select()37    .from(qualityGateResults)38    .where(eq(qualityGateResults.runId, runId))39    .orderBy(desc(qualityGateResults.createdAt), asc(qualityGateResults.gate));40  const evalIds = [...new Set(gates.map((g) => g.evaluationId))];41  const findings = await d42    .select()43    .from(reviewFindings)44    .where(eq(reviewFindings.runId, runId))45    .orderBy(asc(reviewFindings.round));46  const decisions = await d47    .select()48    .from(publicationDecisions)49    .where(eq(publicationDecisions.runId, runId))50    .orderBy(desc(publicationDecisions.createdAt));51  const latest = gates.filter((g) => g.evaluationId === evalIds[0]);52  const failedHard = latest.filter((g) => g.severity === "hard" && g.status === "fail");53  const blocked = failedHard.filter((g) => NON_OVERRIDABLE.includes(g.gate));54  return (55    <div className="space-y-6">56      <header>57        <p className="eyebrow">58          <Link href="/admin/quality">Quality</Link>59        </p>60        <h1 className="title-2 mt-1">{data.run.title}</h1>61        <p className="muted flex items-center gap-2 text-sm">62          <StatusBadge status={data.run.status} /> policy {data.run.qualityPolicyVersion} ·{" "}63          <Link href={`/admin/runs/${runId}`}>run</Link> ·{" "}64          <a href={`/api/admin/runs/${runId}/artifacts/review/qa-report.json`}>qa-report.json</a>65        </p>66      </header>67      {gates.length === 0 && (68        <div className="surface p-8 text-center muted">69          No evaluation yet. The scheduler evaluates runs in quality_check.70        </div>71      )}72      {evalIds.map((id, i) => {73        const g = gates.filter((x) => x.evaluationId === id);74        return (75          <section key={id} className="surface p-4" aria-labelledby={`e-${id}`}>76            <h2 id={`e-${id}`} className="title-3">77              Evaluation {i === 0 ? "(latest)" : ""}{" "}78              <span className="subtle font-mono text-xs">79                {id.slice(0, 8)} · {fmtDateTime(g[0]?.createdAt)}80              </span>81            </h2>82            <p className="muted text-xs">83              {g.filter((x) => x.status === "pass").length} pass ·{" "}84              {g.filter((x) => x.status === "fail").length} fail ·{" "}85              {g.filter((x) => x.status === "warn").length} warn ·{" "}86              {g.filter((x) => x.status === "skip").length} skip87            </p>88            <table className="table mt-2">89              <thead>90                <tr>91                  <th>Gate</th>92                  <th>Severity</th>93                  <th>Status</th>94                  <th>Message</th>95                  <th>Evidence</th>96                </tr>97              </thead>98              <tbody>99                {g.map((x) => (100                  <tr key={x.id}>101                    <td className="font-mono text-xs">102                      {x.gate}103                      {NON_OVERRIDABLE.includes(x.gate) && (104                        <span className="subtle block text-[10px]">non-overridable</span>105                      )}106                    </td>107                    <td>{x.severity}</td>108                    <td>109                      <StatusBadge status={x.status} />110                    </td>111                    <td className="text-xs">{x.message}</td>112                    <td className="text-xs">113                      {Object.keys(x.evidence).length ? (114                        <details>115                          <summary className="cursor-pointer subtle">show</summary>116                          <pre className="max-w-md whitespace-pre-wrap">117                            {JSON.stringify(x.evidence, null, 1)}118                          </pre>119                        </details>120                      ) : (121                        "—"122                      )}123                    </td>124                  </tr>125                ))}126              </tbody>127            </table>128          </section>129        );130      })}131      {findings.length > 0 && (132        <section className="surface p-4">133          <h2 className="title-3">Reviewer findings</h2>134          <ul className="mt-2 space-y-2 text-sm">135            {findings.map((f) => (136              <li key={f.id}>137                <StatusBadge status={f.severity} /> <strong>{f.title}</strong>{" "}138                <span className="subtle text-xs">139                  round {f.round} · {f.category} · {f.resolution}140                </span>141                <p className="muted text-xs">{f.detail}</p>142                {f.proposedTest && <p className="text-xs">Proposed test: {f.proposedTest}</p>}143              </li>144            ))}145          </ul>146        </section>147      )}148      <section className="surface p-4">149        <h2 className="title-3">Publication decisions</h2>150        {decisions.length === 0 ? (151          <p className="muted text-sm">None.</p>152        ) : (153          <ul className="mt-2 space-y-1 text-sm">154            {decisions.map((x) => (155              <li key={x.id}>156                <StatusBadge157                  status={x.decision === "publish" ? "pass" : x.decision === "reject" ? "fail" : "warn"}158                />{" "}159                <strong>{x.decision}</strong> by {x.decidedBy} ·{" "}160                <span className="subtle text-xs">{fmtDateTime(x.createdAt)}</span>161                {x.override && <span className="badge ml-1">override</span>}162                <p className="muted text-xs">{x.rationale}</p>163              </li>164            ))}165          </ul>166        )}167      </section>168      {data.run.status === "quality_failed" && (169        <section className="surface p-4">170          <h2 className="title-3">Authorized override</h2>171          {blocked.length > 0 ? (172            <p className="text-sm" style={{ color: "var(--color-danger-500)" }}>173              Cannot override: scientific blockers failed ({blocked.map((b) => b.gate).join(", ")}). Fix the174              research and retry the run instead.175            </p>176          ) : (177            <OverrideForm runId={runId} gates={failedHard.map((x) => x.gate)} />178          )}179        </section>180      )}181    </div>182  );183}184
Files · main
Menu

Theme